{
 "cells": [
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "# 问题：简述Python内存机制\n"
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {
    "collapsed": true
   },
   "source": [
    "面试官想从这道题中考察面试人什么？\n",
    "对于 Python 程序员而言，很少去考虑内存问题，因为 Python 自己会去管理内存。一般情况下，也确实是如此，但是在大型项目中，尤其是要处理大量数据的情况下，我们还是要考虑一下内存的问题。但是，考官并不是想让你把 Python 的内存管理详细描述出来，你只需要大概了解简单描述一下即可，因为真正要掌握 Python 的内存管理，那必须去读 Python 的源码了。\n",
    "\n",
    "答案\n",
    "内存机制主要包括垃圾收集和内存管理两部分。Python 主要使用基于引用计数的垃圾回收机制，用于回收一些不再被使用的内存，以及内存池，用于管理小块内存的申请和释放。\n",
    "\n",
    "拓展\n",
    "先从垃圾回收机制说起。为了内存被高效利用，那么 Python 的垃圾收集机制主要分为两阶段：垃圾检测和垃圾回收。\n",
    "\n",
    "垃圾检测：引用计数\n",
    "\n",
    "当给一个对象分配一个新名称或者将一个对象放入一个容器（列表、元组或字典）时，该对象的引用计数都会增加。\n",
    "\n",
    "当对象的引用计数归零后，垃圾回收程序会将对象销毁。\n",
    "\n",
    "引用计数怎样会减少呢？对一个变量重新赋值，即那个名称标签指向了另一个对象，从而原先的对象的引用计数减少一次。我们还可以使用del语句。注意：del语句并不会销毁对象，而是删除名称而已，也就是说，那个名称所指的对象的引用次数减少了一次。\n",
    "\n",
    "下面我们借助sys.getrefcount()函数来了解对象的引用计数具体是怎么一回事？\n",
    "\n",
    "import sys\n",
    "a = []                    # 创建一个空列表对象，a指向这个对象\n",
    "print(sys.getrefcount(a)) # 两次引用，一次来自 a，一次来自 getrefcount\n",
    "b = a\n",
    "print(sys.getrefcount(a)) # 三次引用，一次来自 a，一次来自 getrefcount，一次来自b\n",
    "print(sys.getrefcount(b)) # 也是三次引用，因为a和b指向同一个对象，所以该对象的引用计数是一样的\n",
    "del a\n",
    "print(sys.getrefcount(b)) # 两次引用，因为去除了a对空列表对象的引用\n",
    "那么，全局域的对象呢？它们的引用计数会归零吗？不会。所以，我们要避免把一些复杂的或大的数据存放在全局域中，因为它们在程序运行阶段都不会被回收。\n",
    "\n",
    "垃圾回收机制\n",
    "\n",
    "有了垃圾回收机制，我们就无需手动去分配内存后再手动销毁内存，这一切都由 Python 自己去检测去回收那些不再被使用的内存。\n",
    "\n",
    "Python 如何利用引用计数去垃圾回收呢？比如，当空列表对象的引用计数变为 0 了，那么该对象就会被当作垃圾回收。引用计数的特点就是高效和易于实现。但是，维护引用计数也是需要消耗资源，我们需要为每个对象保存引用计数。另外，每一次赋值我们就需要重新计算引用计数，这里面就会有另一个问题，比如一个变量 a 引用了另一个变量 b，而 b 又引用了 a，那么他俩的引用计数就永远不会归零，这个就是循环引用的问题。对于这些缺点，Python 又引入了“标记-清除”算法和“分代回收”的垃圾回收机制，它们用来解决以上两个问题，这里就不一一详述了。Python 还是主要使用引用计数，以“标记-清除的回收机制”和“分代回收”为辅的机制。\n",
    "\n",
    "垃圾回收时，Python 不能进行其它的任务，频繁的垃圾回收将大大降低 Python 的工作效率。Python 只会在特定条件下，自动启动垃圾回收。当 Python 运行时，会记录其中分配对象（object allocation）和取消分配对象（object deallocation）的次数。当两者的差值高于某个阈值时，垃圾回收才会启动。另外，也可以通过gc.collect()手动回收垃圾。\n",
    "\n",
    "内存管理\n",
    "\n",
    "一旦引用计数下降到 0，对象有一个特定的释放功能，称为\"释放\"内存，以便其他对象可以使用它。这里的“释放”并不是真实地释放给操作系统，而是Python会暂时保留它，并根据需要重用它。在许多情况下，这是一个很好的策略，因为它最大限度地减少了内存分配中涉及的操作系统开销。但是，如果 Python 进程长时间运行，它将占用所需的最大内存量。如果应用程序的峰值内存使用量远远大于其平均使用量，则这是浪费，并可能损害整体系统性能以及应用程序本身的性能。\n",
    "\n",
    "所以，Python 设计了这样的内存管理体系，它由 4 个层次组成：\n",
    "\n",
    "第0层：操作系统的内存管理接口\n",
    "第1层：包装层，用于统一各个操作系统的运行行为，保证了兼容性\n",
    "第2层：创建Python对象的接口\n",
    "第3层：对象缓存池机制\n",
    "\n",
    "下面讨论的都是基于C开发的Python，当然也有基于Java开发JPython，这里不做解释。上面4个层次除了第0层，其它3个层次都是由Python维护。\n",
    "\n",
    "第 0 层是操作系统的一些接口，比如开内存（malloc）和释放内存（free），确保专用堆中有足够的空间来存储所有与 Python 相关的数据；第 1 层和第 2 层是内存池，有 Python 的接口函数PyMem_Malloc函数实现，当对象小于 256K 时，该层直接分配内存；第 3 层是最上层，也就是我们对 Python 对象的直接操作。而真正的垃圾回收就是在第 2 层中实现的。\n",
    "\n",
    "在 C 中，如果频繁的调用 malloc 与 free 时，是会产生性能问题的。再加上频繁的分配与释放小块的内存会产生很多内存碎片。Python 在这里主要干的工作有：如果请求分配的内存在 1~256 字节之间就使用自己的内存管理系统，否则直接使用 malloc。这里还是会调用 malloc 分配内存，但每次会分配一块大小为 256k 的大块内存。经由内存池登记的内存到最后还是会回收到内存池。并不会调用 C 的 free 释放掉，这样以便下次使用。\n",
    "\n",
    "小结\n",
    "Python 的一切都是对象，对象与引用是分离的。这样就导致了有的拷贝只是拷贝引用，即拷贝和被拷贝的名称都指向同一个对象，而深拷贝则是创建新的副本，即开辟了新空间存放同样大小的一个对象。\n",
    "\n",
    "Python 的垃圾回收机制使得缩短了开发过程，但懂得了 Python 是如何分配内存以及如何回收，才能够让我们的程序高效使用内存。\n",
    "\n"
   ]
  }
 ],
 "metadata": {
  "kernelspec": {
   "display_name": "Python 3",
   "language": "python",
   "name": "python3"
  },
  "language_info": {
   "codemirror_mode": {
    "name": "ipython",
    "version": 3
   },
   "file_extension": ".py",
   "mimetype": "text/x-python",
   "name": "python",
   "nbconvert_exporter": "python",
   "pygments_lexer": "ipython3",
   "version": "3.8.3"
  }
 },
 "nbformat": 4,
 "nbformat_minor": 1
}